import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Utility class for configuration and validation of configurable objects.
 * <p>
 * A configurable object is an object that exposes configurable properties. A
 * configurable property is a simple field annotated with the {@link Property}
 * annotation. The values of the properties (i.e. fields) are set using
 * introspection. There are two ways to configure the value of a configurable
 * field:
 * <p>
 * <ol>
 * <li>If the field type is class that provides either a string constructor
 * (i.e. a constructor with a single {@code String} parameter) or a static
 * factory method {@code valueOf (String)} returning an instance of the class,
 * then the value of the field is set to an instance of the field type created
 * from a string representation of the value.</li>
 * <li>If creating instances of the field type is more complex, the field value
 * can be set through a method annotated with the {@link Setter} annotation.
 * Such a method must have a setter signature, i.e. it must return {@code void}
 * and accept a single {@code String} parameter.</li>
 * </ol>
 * <p>
 * Configurable properties of an object can be set using the
 * {@link #set(Object, String, String) set()} method, while validation of object
 * properties can be achieved using the {@link #check(Object) check()} method.
 * The validation ensures that no field representing a mandatory configurable
 * property contains {@code null}.
 */
public class Configurator {

   /* ***********************************************************************
    * String tools and constants
    * **********************************************************************
    */

   /**
    * Prefix of setter method.
    */
   private static String KEYWORD_SET = "set";

   /**
    * Check if string is empty.
    * <p>
    * Function returns true if reference to text is null or length of text is 0.
    * </p>
    * 
    * @param text
    *           is text to check.
    * @return true if text is empty, otherwise false.
    */
   private static boolean stringIsEmpty(String text) {
      if (text == null)
         return true;
      if (text.length() == 0)
         return true;
      return false;
   }

   /* ***********************************************************************
    * ANNOTATIONS
    * **********************************************************************
    */

   /**
    * Interface for representing property values.
    */
   @Documented
   @Retention(RetentionPolicy.RUNTIME)
   @Target(ElementType.FIELD)
   public @interface Property {

      /**
       * Returns the name of the configuration property held in the annotated
       * field.
       * 
       * @return name of the property held in the annotated field, or empty
       *         string indicating that the name is identical to the field name
       */
      String name() default "";

      /**
       * Determines whether the property is required or not.
       * 
       * @return {@code true} if the property must have a non-null value,
       *         {@code false} otherwise
       */
      boolean required() default true;

      /**
       * Returns the string representation of the default value of the annotated
       * field. Empty string indicates that the default value has not been set.
       * 
       * @return string representation of the default value
       */
      String defaultValue() default "";

      /**
       * Returns an optional description of the property held in the annotated
       * field.
       * 
       * @return name
       */
      String description() default "";
   }

   /**
    * Interface for getting name of property configured by the annotated method.
    */
   @Documented
   @Retention(RetentionPolicy.RUNTIME)
   @Target(ElementType.METHOD)
   public @interface Setter {

      /**
       * Returns the name of the property configured by the annotated method.
       * 
       * @return name of the property configured by the annotated method, or
       *         empty string indicating that the name corresponds to method
       *         name without the "set" prefix with the first letter in lower
       *         case
       */
      String name() default "";

   }

   /* ***********************************************************************
    * PUBLIC METHODS
    * **********************************************************************
    */

   /**
    * Sets the given property to the given value in the given configurable
    * object.
    * 
    * @param targetObjcet
    *           target configurable object to set the property on
    * @param propertyName
    *           property name
    * @param newPropertyValue
    *           property value
    * @throws ConfExc
    *            if the value of the given property cannot be set on the given
    *            object
    */
   public static void set(Object targetObject, String propertyName,
         String newPropertyValue) throws ConfExc {

      // First try to obtain a method based setter, which is necessary
      // for more complex properties.
      PropertySetter setterMethod = mkMethodSetter(targetObject, propertyName);

      // If that fails, try to obtain a
      // field based setter, assuming the property is simple enough to
      // convert to object instance.
      if (setterMethod == null) {
         setterMethod = getFieldPropertySetter(propertyName, targetObject);
      }

      // If it works, set the property value, otherwise log a warning.
      if (setterMethod != null) {
         setterMethod.setValue(newPropertyValue);
      } else {
         traceWithLevel(Level.WARNING,
               "Unable to find configuration method for property %s",
               propertyName);
      }

   }

   /**
    * Checks if all configurable fields in the passed object are not null.
    * 
    * @param objectToModify
    *           object with configurable fields
    * @throws ConfExc
    *            if property has wrong configuration. Also if unexpected
    *            exception occurs, it will be wrapped into ConfExc exception.
    */
   public static void check(Object objectToModify) throws ConfExc {
      //
      // Find all configurable fields and make sure that all mandatory
      // fields have a non-null value. If any configurable field with
      // a null value is found and has a default value specified, an
      // instance of the value is created and the field is initialized
      // to the default value.
      //
      // If any mandatory field remains uninitialized, throw an exception.
      //

      try {
         for (Field currentField : new AllDeclFieldsIterable(
               objectToModify.getClass())) {
            //
            // Skip fields without the @Property annotation or fields
            // with non-null value.
            //
            Property currentFieldProperty = currentField
                  .getAnnotation(Property.class);

            if (currentFieldProperty != null) {

               //
               // Make the field accessible before getting its value and
               // restore the previous accessibility state after that.
               //
               boolean originalObjectAccess = currentField.isAccessible();
               currentField.setAccessible(true);
               Object valueObject = currentField.get(objectToModify);
               currentField.setAccessible(originalObjectAccess);

               //
               // Set default value for null fields.
               //
               if (valueObject == null) {

                  String currentPropertyName = currentFieldProperty.name();
                  if (stringIsEmpty(currentPropertyName)) {
                     currentPropertyName = currentField.getName();
                  }

                  String currentPropertyNewValue = currentFieldProperty
                        .defaultValue();
                  if (stringIsEmpty(currentPropertyNewValue)) {
                     currentPropertyNewValue = null;
                  }

                  if (currentPropertyNewValue != null) {
                     trace("setting field property %s to default value %s",
                           currentPropertyName, currentPropertyNewValue);

                     configureFieldProperty(currentPropertyName,
                           objectToModify, currentPropertyNewValue,
                           currentField);

                  } else if (currentFieldProperty.required()) {

                     // property is required, but value is null
                     throw new ConfExc(
                           "Required property '%s' is not configured",
                           currentPropertyName);
                  }
               }
            }

         }

      } catch (ConfExc configuratorException) {
         // propagate without wrapping
         throw configuratorException;

      } catch (Exception e) {
         wrapThrowableAsConfExc(e,
               "Unable to verify object property configuration!");
      }
   }

   /* ***********************************************************************
    * PropertySetter
    * **********************************************************************
    */

   /**
    * Private interface encapsulating a method for setting a property value on
    * an object. Instances of the {@link PropertySetter} interface are bound to
    * a particular target object and property name at construction time -- only
    * the value remains unbound and can be set.
    */
   private interface PropertySetter {
      void setValue(String inputValue);
   }

   /* ***********************************************************************
    * Field-based property setter
    * **********************************************************************
    */

   /**
    * Returns a field based {@link PropertySetter} bound to the given object and
    * property name. When setting the property value, the returned
    * {@link PropertySetter} will modify the value of an object field annotated
    * by the {@code @Property} annotation with matching name.
    * 
    * @param propertyName
    *           name of the property to set
    * @param targetObject
    *           target object on which to set the property
    * 
    * @return {@link PropertySetter} which allows to configure the property on
    *         the given object, or {@code null} if the target object has no
    *         field with matching annotation. Returned {@link PropertySetter}
    *         may throw {@link ConExc} exception.
    */
   protected static PropertySetter getFieldPropertySetter(
         final String propertyName, final Object targetObject) {
      //
      // Find a configurable field for the given property and create a
      // PropertySetter for the property.
      //
      // Specifically, enumerate all fields in the given object's class
      // hierarchy and find the first field annotated with the
      // @Property annotation matching the given property field.
      //
      for (final Field currentField : new AllDeclFieldsIterable(
            targetObject.getClass())) {
         String currentPropertyName = null;

         Property currentFieldProperty = currentField
               .getAnnotation(Property.class);

         if (currentFieldProperty == null) {
            currentPropertyName = null;
         } else if (currentFieldProperty.name().length() > 0) {
            currentPropertyName = currentFieldProperty.name();
         } else {
            currentPropertyName = currentField.getName();
         }

         if (propertyName.equals(currentPropertyName)) {
            //
            // Match found -- create and return the setter.
            //
            return new PropertySetter() {
               public void setValue(String newValue) throws ConfExc {
                  trace("setting field property %s to %s", propertyName,
                        newValue);
                  configureFieldProperty(propertyName, targetObject, newValue,
                        currentField);
               }
            };
         }
      }

      //
      // No match found.
      //
      return null;
   }

   /**
    * Sets the given field of the given object to the given value. The value is
    * converted from string representation to an instance of the field type.
    * 
    * @param propertyName
    *           name of the property being configured
    * @param targetObject
    *           target object on which to set the field value
    * @param newValue
    *           string value of the property being configured
    * @param fieldName
    *           the field to set to the given value
    * 
    * @throws ConfExc
    *            if the string representation of the value could not be
    *            converted to an instance of the field type or if setting the
    *            field failed
    */
   protected static void configureFieldProperty(String propertyName,
         Object targetObject, String newValue, Field fieldName) throws ConfExc {
      //
      // Create an instance of the property value and set the field
      // value on the target object.
      //
      Object instanceOfNewValue = makeValueInstance(fieldName, newValue);
      if (instanceOfNewValue == null) {
         throw new ConfExc("property %s: could not create %s instance for %s",
               propertyName, fieldName.getType().getName(), newValue);
      }

      try {
         //
         // Make the field accessible before setting its value and
         // restore the previous accessibility state after that.
         //
         boolean originalObjectAccess = fieldName.isAccessible();
         fieldName.setAccessible(true);
         fieldName.set(targetObject, instanceOfNewValue);
         fieldName.setAccessible(originalObjectAccess);
      } catch (Exception e) {
         wrapThrowableAsConfExc(e,
               "Unable to configure field %s with property %s=%s",
               fieldName.getName(), propertyName, newValue);
      }
   }

   /**
    * Try to create the value instance by invoking a constructor of the field
    * class
    * 
    * @param fieldName
    *           field to create value for
    * @param fieldValueString
    *           string representation of field value
    * @return Null if creating of the new instance failed. Otherwise function
    *         return new instance.
    */
   private static Object createInstanceByConstructor(Field fieldName,
         String fieldValueString) {
      try {
         Class<?> fieldType = fieldName.getType();
         Constructor<?> constructorOfObject = fieldType
               .getConstructor(new Class<?>[] { String.class });
         return constructorOfObject.newInstance(fieldValueString);
      } catch (Exception e) {
         traceWithLevel(Level.FINE,
               "Instance of new object cound not be created by constructor, "
                     + "because : %s", e.getMessage());
      }
      return null;
   }

   /**
    * Try to create the value instance by invoking a class factory.
    * 
    * @param fieldName
    *           field to create value for
    * @param fieldValueString
    *           string representation of field value
    * @return Null if creating of the new instance failed. Otherwise function
    *         return new instance.
    */
   private static Object createInstanceByFactory(Field fieldName,
         String fieldValueString) {
      try {

         Class<?> fieldType = (Class<?>) fieldName.getType();
         Method factory = fieldType.getMethod("valueOf",
               new Class<?>[] { String.class });
         Class<?> factoryResultType = factory.getReturnType();
         boolean fieldAndFactoryMatches = fieldType
               .isAssignableFrom(factoryResultType);

         if (fieldAndFactoryMatches)
            return factory.invoke(null, fieldValueString);
      } catch (Exception e) {
         traceWithLevel(Level.FINE,
               "Instance of new object cound not be created by factory, "
                     + "because : %s", e.getMessage());
      }
      return null;
   }

   /**
    * Creates an object instance from the string representation of a property
    * value. The instance type is determined by the type of the given field and
    * the instance is created either by calling a string constructor or a static
    * factory method on the field class.
    * 
    * @param fieldName
    *           field to create value for
    * @param fieldValueString
    *           string representation of field value
    * @return an object of the given field type representing the given value, or
    *         {@code null} if the instance could not be created
    */
   protected static Object makeValueInstance(Field fieldName,
         String fieldValueString) {

      // First try to create the value instance by invoking a string
      // constructor of the field class.
      Object result = createInstanceByConstructor(fieldName, fieldValueString);

      // If there is no suitable constructor, try to create the instance by
      // invoking a static factory method.
      if (result == null)
         result = createInstanceByFactory(fieldName, fieldValueString);

      // Could not create the instance, return null.
      return null;
   }

   /* ***********************************************************************
    * Method-based property setter
    * **********************************************************************
    */

   /**
    * Returns a method based {@link PropertySetter} bound to the given object
    * and property name. When setting the property value, the returned
    * {@link PropertySetter} will invoke a setter method annotated by the
    * {@link Setter} annotation with matching name.
    * 
    * @param targetObject
    *           target object on which to set the property
    * @param propertyName
    *           name of the property to set
    * @return {@link PropertySetter} which allows to configure the property on
    *         the given object, or {@code null} if the target object has no
    *         setter method with matching annotation
    */
   protected static PropertySetter mkMethodSetter(final Object targetObject,
         final String propertyName) {

      //
      // Find a setter method for the given property and create a
      // PropertySetter for the property.
      //
      // Specifically, enumerate all methods in the given object's class
      // hierarchy and find the first setter method annotated with the
      // @Setter annotation matching the given property field.
      //

      Class<?> targetClass = targetObject.getClass();

      while (targetClass != null) {
         for (final Method currentDeclaredMethod : targetClass
               .getDeclaredMethods()) {

            Setter currentPropertySetter = currentDeclaredMethod
                  .getAnnotation(Setter.class);
            String currentMethodName = null;

            // get setter-method name from property setter
            // annotation or setter name
            if (currentPropertySetter != null) {

               currentMethodName = currentPropertySetter.name();
               if (stringIsEmpty(currentMethodName)) {

                  // extract method name from declared method
                  currentMethodName = extractSetterName(currentDeclaredMethod
                        .getName());
               }
            }
            if (propertyName.equals(currentMethodName)) {
               //
               // Match found -- create the setter.
               //
               return createPropertySetter(targetObject, propertyName,
                     currentDeclaredMethod);

            }
         }
         targetClass = targetClass.getSuperclass();
      }

      //
      // No match found.
      //
      return null;
   }

   /**
    * Extract setter method name.
    * 
    * If method starts with "set", it will extract method name with lower-case
    * first letter of method.
    * 
    * @param methodName
    *           name of method
    * @return extracted method name, if method starts with "set", otherwise it
    *         returns original method name
    */
   private static String extractSetterName(String methodName) {
      String result = methodName;
      if (methodName.startsWith(KEYWORD_SET)) {

         String methodNameExceptFirstLetter = methodName.substring(KEYWORD_SET
               .length() + 1);
         String firstLetterOfMethod = methodName.substring(
               KEYWORD_SET.length(), KEYWORD_SET.length() + 1);

         result = firstLetterOfMethod.toLowerCase()
               + methodNameExceptFirstLetter;
      }
      return result;

   }

   /**
    * Method will create new property setter.
    * 
    * @param targetObject
    *           target object on which to create the property setter
    * @param propertyName
    *           name of property
    * @param currentDeclaredMethod
    */
   private static PropertySetter createPropertySetter(
         final Object targetObject, final String propertyName,
         final Method currentDeclaredMethod) {

      // creating instance of new property setter

      return new PropertySetter() {

         //
         // Implementation of method setValue
         //
         public void setValue(String newValue) {

            trace("setting method property %s to %s", propertyName, newValue);
            
            // get method basic properties
            boolean methodResultIsVoid = (Class<?>) currentDeclaredMethod
                  .getReturnType() != void.class;
            boolean firstArgumentIsNotString = currentDeclaredMethod
                  .getParameterTypes()[0] != String.class;
            boolean methodHasNotOneArguments = currentDeclaredMethod
                  .getParameterTypes().length != 1;

            // according to properties check, if method is setter
            if (methodResultIsVoid || firstArgumentIsNotString
                  || methodHasNotOneArguments)
               throw new ConfExc("property %s: method %s() is not a setter",
                     propertyName, currentDeclaredMethod.getName());
            try {
               boolean originalObjectAccess = currentDeclaredMethod
                     .isAccessible();
               currentDeclaredMethod.setAccessible(true);
               currentDeclaredMethod.invoke(targetObject, newValue);
               currentDeclaredMethod.setAccessible(originalObjectAccess);
            } catch (Exception e) {
               wrapThrowableAsConfExc(e,
                     "Unable to set property %s=%s using method %s()",
                     propertyName, newValue, currentDeclaredMethod.getName());
            }
         }
      };

   }

   /* ***********************************************************************
    * Logging
    * **********************************************************************
    */

   /**
    * Object used for tracing messages.
    */
   public static Logger log = Logger.getAnonymousLogger();

   /**
    * Method will trace message in FINE level.
    * 
    * @param formatString
    *           The string message (or formating string of message)
    * @param arguments
    *           Array of parameters into string format
    */
   private static void trace(String formatString, Object... arguments) {
      traceWithLevel(Level.FINE, formatString, arguments);
   }

   /**
    * Trace message in specific level. Method will trace message, only if given
    * log level is available.
    * 
    * @param logLevel
    *           One of message level identifiers
    * @param formatString
    *           The string message (or formating string of message)
    * @param arguments
    *           Array of parameters into string format
    */
   private static void traceWithLevel(Level logLevel, String formatString,
         Object... arguments) {
      if (log.isLoggable(logLevel))
         log.log(logLevel, formatString, arguments);
   }

   /* ***********************************************************************
    * ConfExc
    * **********************************************************************
    */

   /**
    * Common exception for all configuration errors.
    */
   public static class ConfExc extends RuntimeException {

      /**
       * Identification for serialization
       */
      private static final long serialVersionUID = -401556123649388614L;

      /**
       * Create new exception with formated message.
       * 
       * @param formatString
       *           a format string
       * @param args
       *           array of objects used in format string
       */
      public ConfExc(String formatString, Object... args) {
         super(String.format(formatString, args));
      }

      /**
       * Create new exception with formated message and exception.
       * 
       * @param throwableException
       *           Exception will be stored into exception and used for getting
       *           reason of exception.
       * @param formatString
       *           a format string
       * @param args
       *           array of objects used in format string
       * 
       */
      public ConfExc(Throwable throwableException, String formatString,
            Object... args) {
         super(String.format(formatString, args), throwableException);
      }
   }

   /**
    * Wraps the given {@link Throwable} as a {@link ConfExc} along with an
    * additional formatted message.
    */
   private static void wrapThrowableAsConfExc(Throwable throwableException,
         String formatString, Object... args) {
      throw new ConfExc(throwableException, formatString, args);
   }

   /* ***********************************************************************
    * AllDeclFieldsIterable
    * **********************************************************************
    */

   /**
    * Provides an iterable interface to fields declared in a class hierarchy.
    * The provided iterator traverses the inheritance hierarchy from the given
    * (leaf) class to the (root) base class and returns all fields declared in
    * the classes along the inheritance path. The iterator (obviously) does not
    * support element removal, since it operates on an immutable structure.
    */
   protected static class AllDeclFieldsIterable implements Iterable<Field> {
      private Class<?> leafClass;

      /**
       * Creates an iterable for the given leaf class. If the leaf class is
       * {@code null}, the iterable produces an empty iterator.
       */
      protected AllDeclFieldsIterable(Class<?> inputLeafClass) {
         leafClass = inputLeafClass;
      }

      @Override
      public Iterator<Field> iterator() {
         return new Iterator<Field>() {

            private Class<?> currentClass = leafClass;

            private Iterator<Field> iterableFields = new ArrayIterator<>(
                  new Field[0]);

            public boolean hasNext() {
               //
               // If there are no fields, check if there is another class
               // where to look for fields. If we run out of classes, there
               // are no more fields left.
               //
               while (!iterableFields.hasNext()) {
                  if (currentClass == null) {
                     return false;
                  }

                  iterableFields = new ArrayIterator<Field>(
                        currentClass.getDeclaredFields());

                  currentClass = currentClass.getSuperclass();
               }

               return true;
            }

            @Override
            public Field next() {
               if (!hasNext())
                  throw new NoSuchElementException();
               return iterableFields.next();
            }

            public void remove() {
               throw new UnsupportedOperationException();
            }
         };
      }
   }

   /**
    * *******************************************************************
    * ArrayIterator
    * ******************************************************************
    */

   protected static class ArrayIterator<E> implements Iterator<E> {
      private E[] arrayToIterate;
      private int arrayLength;
      private int currentPosition;

      protected ArrayIterator(E[] inputArray) {
         arrayToIterate = inputArray;
         arrayLength = inputArray.length;
         currentPosition = 0;
      }

      @Override
      public void remove() {
         throw new UnsupportedOperationException(
               "cannot remove elements from array");
      }

      @Override
      public boolean hasNext() {
         return currentPosition < arrayLength;
      }

      @Override
      public E next() {
         if (!hasNext())
            throw new NoSuchElementException();
         return arrayToIterate[currentPosition++];
      }
   }

}
